/*
 * Copyright (c) 2002, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package javax.swing.plaf.synth;

import javax.swing.*;
import javax.swing.plaf.*;
import javax.swing.plaf.basic.*;
import javax.swing.text.View;

import java.awt.*;
import java.awt.event.*;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeEvent;
import sun.swing.SwingUtilities2;

/**
 * Provides the Synth L&amp;F UI delegate for
 * {@link javax.swing.JTabbedPane}.
 *
 * <p>Looks up the {@code selectedTabPadInsets} property from the Style,
 * which represents additional insets for the selected tab.
 *
 * @author Scott Violet
 * @since 1.7
 */
public class SynthTabbedPaneUI extends BasicTabbedPaneUI
    implements PropertyChangeListener, SynthUI {

  /**
   * <p>If non-zero, tabOverlap indicates the amount that the tab bounds
   * should be altered such that they would overlap with a tab on either the
   * leading or trailing end of a run (ie: in TOP, this would be on the left
   * or right).</p>
   *
   * <p>A positive overlap indicates that tabs should overlap right/down,
   * while a negative overlap indicates tha tabs should overlap left/up.</p>
   *
   * <p>When tabOverlap is specified, it both changes the x position and width
   * of the tab if in TOP or BOTTOM placement, and changes the y position and
   * height if in LEFT or RIGHT placement.</p>
   *
   * <p>This is done for the following reason. Consider a run of 10 tabs.
   * There are 9 gaps between these tabs. If you specified a tabOverlap of
   * "-1", then each of the tabs "x" values will be shifted left. This leaves
   * 9 pixels of space to the right of the right-most tab unpainted. So, each
   * tab's width is also extended by 1 pixel to make up the difference.</p>
   *
   * <p>This property respects the RTL component orientation.</p>
   */
  private int tabOverlap = 0;

  /**
   * When a tabbed pane has multiple rows of tabs, this indicates whether
   * the tabs in the upper row(s) should extend to the base of the tab area,
   * or whether they should remain at their normal tab height. This does not
   * affect the bounds of the tabs, only the bounds of area painted by the
   * tabs. The text position does not change. The result is that the bottom
   * border of the upper row of tabs becomes fully obscured by the lower tabs,
   * resulting in a cleaner look.
   */
  private boolean extendTabsToBase = false;

  private SynthContext tabAreaContext;
  private SynthContext tabContext;
  private SynthContext tabContentContext;

  private SynthStyle style;
  private SynthStyle tabStyle;
  private SynthStyle tabAreaStyle;
  private SynthStyle tabContentStyle;

  private Rectangle textRect = new Rectangle();
  private Rectangle iconRect = new Rectangle();

  private Rectangle tabAreaBounds = new Rectangle();

  //added for the Nimbus look and feel, where the tab area is painted differently depending on the
  //state for the selected tab
  private boolean tabAreaStatesMatchSelectedTab = false;
  //added for the Nimbus LAF to ensure that the labels don't move whether the tab is selected or not
  private boolean nudgeSelectedLabel = true;

  private boolean selectedTabIsPressed = false;

  /**
   * Creates a new UI object for the given component.
   *
   * @param c component to create UI object for
   * @return the UI object
   */
  public static ComponentUI createUI(JComponent c) {
    return new SynthTabbedPaneUI();
  }

  private boolean scrollableTabLayoutEnabled() {
    return (tabPane.getTabLayoutPolicy() == JTabbedPane.SCROLL_TAB_LAYOUT);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected void installDefaults() {
    updateStyle(tabPane);
  }

  private void updateStyle(JTabbedPane c) {
    SynthContext context = getContext(c, ENABLED);
    SynthStyle oldStyle = style;
    style = SynthLookAndFeel.updateStyle(context, this);
    // Add properties other than JComponent colors, Borders and
    // opacity settings here:
    if (style != oldStyle) {
      tabRunOverlay =
          style.getInt(context, "TabbedPane.tabRunOverlay", 0);
      tabOverlap = style.getInt(context, "TabbedPane.tabOverlap", 0);
      extendTabsToBase = style.getBoolean(context,
          "TabbedPane.extendTabsToBase", false);
      textIconGap = style.getInt(context, "TabbedPane.textIconGap", 0);
      selectedTabPadInsets = (Insets) style.get(context,
          "TabbedPane.selectedTabPadInsets");
      if (selectedTabPadInsets == null) {
        selectedTabPadInsets = new Insets(0, 0, 0, 0);
      }
      tabAreaStatesMatchSelectedTab = style.getBoolean(context,
          "TabbedPane.tabAreaStatesMatchSelectedTab", false);
      nudgeSelectedLabel = style.getBoolean(context,
          "TabbedPane.nudgeSelectedLabel", true);
      if (oldStyle != null) {
        uninstallKeyboardActions();
        installKeyboardActions();
      }
    }
    context.dispose();

    if (tabContext != null) {
      tabContext.dispose();
    }
    tabContext = getContext(c, Region.TABBED_PANE_TAB, ENABLED);
    this.tabStyle = SynthLookAndFeel.updateStyle(tabContext, this);
    tabInsets = tabStyle.getInsets(tabContext, null);

    if (tabAreaContext != null) {
      tabAreaContext.dispose();
    }
    tabAreaContext = getContext(c, Region.TABBED_PANE_TAB_AREA, ENABLED);
    this.tabAreaStyle = SynthLookAndFeel.updateStyle(tabAreaContext, this);
    tabAreaInsets = tabAreaStyle.getInsets(tabAreaContext, null);

    if (tabContentContext != null) {
      tabContentContext.dispose();
    }
    tabContentContext = getContext(c, Region.TABBED_PANE_CONTENT, ENABLED);
    this.tabContentStyle = SynthLookAndFeel.updateStyle(tabContentContext,
        this);
    contentBorderInsets =
        tabContentStyle.getInsets(tabContentContext, null);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected void installListeners() {
    super.installListeners();
    tabPane.addPropertyChangeListener(this);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected void uninstallListeners() {
    super.uninstallListeners();
    tabPane.removePropertyChangeListener(this);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected void uninstallDefaults() {
    SynthContext context = getContext(tabPane, ENABLED);
    style.uninstallDefaults(context);
    context.dispose();
    style = null;

    tabStyle.uninstallDefaults(tabContext);
    tabContext.dispose();
    tabContext = null;
    tabStyle = null;

    tabAreaStyle.uninstallDefaults(tabAreaContext);
    tabAreaContext.dispose();
    tabAreaContext = null;
    tabAreaStyle = null;

    tabContentStyle.uninstallDefaults(tabContentContext);
    tabContentContext.dispose();
    tabContentContext = null;
    tabContentStyle = null;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public SynthContext getContext(JComponent c) {
    return getContext(c, SynthLookAndFeel.getComponentState(c));
  }

  private SynthContext getContext(JComponent c, int state) {
    return SynthContext.getContext(c, style, state);
  }

  private SynthContext getContext(JComponent c, Region subregion, int state) {
    SynthStyle style = null;

    if (subregion == Region.TABBED_PANE_TAB) {
      style = tabStyle;
    } else if (subregion == Region.TABBED_PANE_TAB_AREA) {
      style = tabAreaStyle;
    } else if (subregion == Region.TABBED_PANE_CONTENT) {
      style = tabContentStyle;
    }
    return SynthContext.getContext(c, subregion, style, state);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected JButton createScrollButton(int direction) {
    // added for Nimbus LAF so that it can use the basic arrow buttons
    // UIManager is queried directly here because this is called before
    // updateStyle is called so the style can not be queried directly
    if (UIManager.getBoolean("TabbedPane.useBasicArrows")) {
      JButton btn = super.createScrollButton(direction);
      btn.setBorder(BorderFactory.createEmptyBorder());
      return btn;
    }
    return new SynthScrollableTabButton(direction);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void propertyChange(PropertyChangeEvent e) {
    if (SynthLookAndFeel.shouldUpdateStyle(e)) {
      updateStyle(tabPane);
    }
  }

  /**
   * {@inheritDoc}
   *
   * Overridden to keep track of whether the selected tab is also pressed.
   */
  @Override
  protected MouseListener createMouseListener() {
    final MouseListener delegate = super.createMouseListener();
    final MouseMotionListener delegate2 = (MouseMotionListener) delegate;
    return new MouseListener() {
      public void mouseClicked(MouseEvent e) {
        delegate.mouseClicked(e);
      }

      public void mouseEntered(MouseEvent e) {
        delegate.mouseEntered(e);
      }

      public void mouseExited(MouseEvent e) {
        delegate.mouseExited(e);
      }

      public void mousePressed(MouseEvent e) {
        if (!tabPane.isEnabled()) {
          return;
        }

        int tabIndex = tabForCoordinate(tabPane, e.getX(), e.getY());
        if (tabIndex >= 0 && tabPane.isEnabledAt(tabIndex)) {
          if (tabIndex == tabPane.getSelectedIndex()) {
            // Clicking on selected tab
            selectedTabIsPressed = true;
            //TODO need to just repaint the tab area!
            tabPane.repaint();
          }
        }

        //forward the event (this will set the selected index, or none at all
        delegate.mousePressed(e);
      }

      public void mouseReleased(MouseEvent e) {
        if (selectedTabIsPressed) {
          selectedTabIsPressed = false;
          //TODO need to just repaint the tab area!
          tabPane.repaint();
        }
        //forward the event
        delegate.mouseReleased(e);

        //hack: The super method *should* be setting the mouse-over property correctly
        //here, but it doesn't. That is, when the mouse is released, whatever tab is below the
        //released mouse should be in rollover state. But, if you select a tab and don't
        //move the mouse, this doesn't happen. Hence, forwarding the event.
        delegate2.mouseMoved(e);
      }
    };
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected int getTabLabelShiftX(int tabPlacement, int tabIndex, boolean isSelected) {
    if (nudgeSelectedLabel) {
      return super.getTabLabelShiftX(tabPlacement, tabIndex, isSelected);
    } else {
      return 0;
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected int getTabLabelShiftY(int tabPlacement, int tabIndex, boolean isSelected) {
    if (nudgeSelectedLabel) {
      return super.getTabLabelShiftY(tabPlacement, tabIndex, isSelected);
    } else {
      return 0;
    }
  }

  /**
   * Notifies this UI delegate to repaint the specified component.
   * This method paints the component background, then calls
   * the {@link #paint(SynthContext, Graphics)} method.
   *
   * <p>In general, this method does not need to be overridden by subclasses.
   * All Look and Feel rendering code should reside in the {@code paint} method.
   *
   * @param g the {@code Graphics} object used for painting
   * @param c the component being painted
   * @see #paint(SynthContext, Graphics)
   */
  @Override
  public void update(Graphics g, JComponent c) {
    SynthContext context = getContext(c);

    SynthLookAndFeel.update(context, g);
    context.getPainter().paintTabbedPaneBackground(context,
        g, 0, 0, c.getWidth(), c.getHeight());
    paint(context, g);
    context.dispose();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected int getBaseline(int tab) {
    if (tabPane.getTabComponentAt(tab) != null ||
        getTextViewForTab(tab) != null) {
      return super.getBaseline(tab);
    }
    String title = tabPane.getTitleAt(tab);
    Font font = tabContext.getStyle().getFont(tabContext);
    FontMetrics metrics = getFontMetrics(font);
    Icon icon = getIconForTab(tab);
    textRect.setBounds(0, 0, 0, 0);
    iconRect.setBounds(0, 0, 0, 0);
    calcRect.setBounds(0, 0, Short.MAX_VALUE, maxTabHeight);
    tabContext.getStyle().getGraphicsUtils(tabContext).layoutText(
        tabContext, metrics, title, icon, SwingUtilities.CENTER,
        SwingUtilities.CENTER, SwingUtilities.LEADING,
        SwingUtilities.CENTER, calcRect,
        iconRect, textRect, textIconGap);
    return textRect.y + metrics.getAscent() + getBaselineOffset();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void paintBorder(SynthContext context, Graphics g, int x,
      int y, int w, int h) {
    context.getPainter().paintTabbedPaneBorder(context, g, x, y, w, h);
  }

  /**
   * Paints the specified component according to the Look and Feel.
   * <p>This method is not used by Synth Look and Feel.
   * Painting is handled by the {@link #paint(SynthContext, Graphics)} method.
   *
   * @param g the {@code Graphics} object used for painting
   * @param c the component being painted
   * @see #paint(SynthContext, Graphics)
   */
  @Override
  public void paint(Graphics g, JComponent c) {
    SynthContext context = getContext(c);

    paint(context, g);
    context.dispose();
  }

  /**
   * Paints the specified component.
   *
   * @param context context for the component being painted
   * @param g the {@code Graphics} object used for painting
   * @see #update(Graphics, JComponent)
   */
  protected void paint(SynthContext context, Graphics g) {
    int selectedIndex = tabPane.getSelectedIndex();
    int tabPlacement = tabPane.getTabPlacement();

    ensureCurrentLayout();

    // Paint tab area
    // If scrollable tabs are enabled, the tab area will be
    // painted by the scrollable tab panel instead.
    //
    if (!scrollableTabLayoutEnabled()) { // WRAP_TAB_LAYOUT
      Insets insets = tabPane.getInsets();
      int x = insets.left;
      int y = insets.top;
      int width = tabPane.getWidth() - insets.left - insets.right;
      int height = tabPane.getHeight() - insets.top - insets.bottom;
      int size;
      switch (tabPlacement) {
        case LEFT:
          width = calculateTabAreaWidth(tabPlacement, runCount,
              maxTabWidth);
          break;
        case RIGHT:
          size = calculateTabAreaWidth(tabPlacement, runCount,
              maxTabWidth);
          x = x + width - size;
          width = size;
          break;
        case BOTTOM:
          size = calculateTabAreaHeight(tabPlacement, runCount,
              maxTabHeight);
          y = y + height - size;
          height = size;
          break;
        case TOP:
        default:
          height = calculateTabAreaHeight(tabPlacement, runCount,
              maxTabHeight);
      }

      tabAreaBounds.setBounds(x, y, width, height);

      if (g.getClipBounds().intersects(tabAreaBounds)) {
        paintTabArea(tabAreaContext, g, tabPlacement,
            selectedIndex, tabAreaBounds);
      }
    }

    // Paint content border
    paintContentBorder(tabContentContext, g, tabPlacement, selectedIndex);
  }

  protected void paintTabArea(Graphics g, int tabPlacement,
      int selectedIndex) {
    // This can be invoked from ScrollabeTabPanel
    Insets insets = tabPane.getInsets();
    int x = insets.left;
    int y = insets.top;
    int width = tabPane.getWidth() - insets.left - insets.right;
    int height = tabPane.getHeight() - insets.top - insets.bottom;

    paintTabArea(tabAreaContext, g, tabPlacement, selectedIndex,
        new Rectangle(x, y, width, height));
  }

  private void paintTabArea(SynthContext ss, Graphics g,
      int tabPlacement, int selectedIndex,
      Rectangle tabAreaBounds) {
    Rectangle clipRect = g.getClipBounds();

    //if the tab area's states should match that of the selected tab, then
    //first update the selected tab's states, then set the state
    //for the tab area to match
    //otherwise, restore the tab area's state to ENABLED (which is the
    //only supported state otherwise).
    if (tabAreaStatesMatchSelectedTab && selectedIndex >= 0) {
      updateTabContext(selectedIndex, true, selectedTabIsPressed,
          (getRolloverTab() == selectedIndex),
          (getFocusIndex() == selectedIndex));
      ss.setComponentState(tabContext.getComponentState());
    } else {
      ss.setComponentState(SynthConstants.ENABLED);
    }

    // Paint the tab area.
    SynthLookAndFeel.updateSubregion(ss, g, tabAreaBounds);
    ss.getPainter().paintTabbedPaneTabAreaBackground(ss, g,
        tabAreaBounds.x, tabAreaBounds.y, tabAreaBounds.width,
        tabAreaBounds.height, tabPlacement);
    ss.getPainter().paintTabbedPaneTabAreaBorder(ss, g, tabAreaBounds.x,
        tabAreaBounds.y, tabAreaBounds.width, tabAreaBounds.height,
        tabPlacement);

    int tabCount = tabPane.getTabCount();

    iconRect.setBounds(0, 0, 0, 0);
    textRect.setBounds(0, 0, 0, 0);

    // Paint tabRuns of tabs from back to front
    for (int i = runCount - 1; i >= 0; i--) {
      int start = tabRuns[i];
      int next = tabRuns[(i == runCount - 1) ? 0 : i + 1];
      int end = (next != 0 ? next - 1 : tabCount - 1);
      for (int j = start; j <= end; j++) {
        if (rects[j].intersects(clipRect) && selectedIndex != j) {
          paintTab(tabContext, g, tabPlacement, rects, j, iconRect,
              textRect);
        }
      }
    }

    if (selectedIndex >= 0) {
      if (rects[selectedIndex].intersects(clipRect)) {
        paintTab(tabContext, g, tabPlacement, rects, selectedIndex,
            iconRect, textRect);
      }
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected void setRolloverTab(int index) {
    int oldRolloverTab = getRolloverTab();
    super.setRolloverTab(index);

    Rectangle r = null;

    if (oldRolloverTab != index && tabAreaStatesMatchSelectedTab) {
      //TODO need to just repaint the tab area!
      tabPane.repaint();
    } else {
      if ((oldRolloverTab >= 0) && (oldRolloverTab < tabPane.getTabCount())) {
        r = getTabBounds(tabPane, oldRolloverTab);
        if (r != null) {
          tabPane.repaint(r);
        }
      }

      if (index >= 0) {
        r = getTabBounds(tabPane, index);
        if (r != null) {
          tabPane.repaint(r);
        }
      }
    }
  }

  private void paintTab(SynthContext ss, Graphics g,
      int tabPlacement, Rectangle[] rects, int tabIndex,
      Rectangle iconRect, Rectangle textRect) {
    Rectangle tabRect = rects[tabIndex];
    int selectedIndex = tabPane.getSelectedIndex();
    boolean isSelected = selectedIndex == tabIndex;
    updateTabContext(tabIndex, isSelected, isSelected && selectedTabIsPressed,
        (getRolloverTab() == tabIndex),
        (getFocusIndex() == tabIndex));

    SynthLookAndFeel.updateSubregion(ss, g, tabRect);
    int x = tabRect.x;
    int y = tabRect.y;
    int height = tabRect.height;
    int width = tabRect.width;
    int placement = tabPane.getTabPlacement();
    if (extendTabsToBase && runCount > 1) {
      //paint this tab such that its edge closest to the base is equal to
      //edge of the selected tab closest to the base. In terms of the TOP
      //tab placement, this will cause the bottom of each tab to be
      //painted even with the bottom of the selected tab. This is because
      //in each tab placement (TOP, LEFT, BOTTOM, RIGHT) the selected tab
      //is closest to the base.
      if (selectedIndex >= 0) {
        Rectangle r = rects[selectedIndex];
        switch (placement) {
          case TOP:
            int bottomY = r.y + r.height;
            height = bottomY - tabRect.y;
            break;
          case LEFT:
            int rightX = r.x + r.width;
            width = rightX - tabRect.x;
            break;
          case BOTTOM:
            int topY = r.y;
            height = (tabRect.y + tabRect.height) - topY;
            y = topY;
            break;
          case RIGHT:
            int leftX = r.x;
            width = (tabRect.x + tabRect.width) - leftX;
            x = leftX;
            break;
        }
      }
    }
    tabContext.getPainter().paintTabbedPaneTabBackground(tabContext, g,
        x, y, width, height, tabIndex, placement);
    tabContext.getPainter().paintTabbedPaneTabBorder(tabContext, g,
        x, y, width, height, tabIndex, placement);

    if (tabPane.getTabComponentAt(tabIndex) == null) {
      String title = tabPane.getTitleAt(tabIndex);
      Font font = ss.getStyle().getFont(ss);
      FontMetrics metrics = SwingUtilities2.getFontMetrics(tabPane, g, font);
      Icon icon = getIconForTab(tabIndex);

      layoutLabel(ss, tabPlacement, metrics, tabIndex, title, icon,
          tabRect, iconRect, textRect, isSelected);

      paintText(ss, g, tabPlacement, font, metrics,
          tabIndex, title, textRect, isSelected);

      paintIcon(g, tabPlacement, tabIndex, icon, iconRect, isSelected);
    }
  }

  private void layoutLabel(SynthContext ss, int tabPlacement,
      FontMetrics metrics, int tabIndex,
      String title, Icon icon,
      Rectangle tabRect, Rectangle iconRect,
      Rectangle textRect, boolean isSelected) {
    View v = getTextViewForTab(tabIndex);
    if (v != null) {
      tabPane.putClientProperty("html", v);
    }

    textRect.x = textRect.y = iconRect.x = iconRect.y = 0;

    ss.getStyle().getGraphicsUtils(ss).layoutText(ss, metrics, title,
        icon, SwingUtilities.CENTER, SwingUtilities.CENTER,
        SwingUtilities.LEADING, SwingUtilities.CENTER,
        tabRect, iconRect, textRect, textIconGap);

    tabPane.putClientProperty("html", null);

    int xNudge = getTabLabelShiftX(tabPlacement, tabIndex, isSelected);
    int yNudge = getTabLabelShiftY(tabPlacement, tabIndex, isSelected);
    iconRect.x += xNudge;
    iconRect.y += yNudge;
    textRect.x += xNudge;
    textRect.y += yNudge;
  }

  private void paintText(SynthContext ss,
      Graphics g, int tabPlacement,
      Font font, FontMetrics metrics, int tabIndex,
      String title, Rectangle textRect,
      boolean isSelected) {
    g.setFont(font);

    View v = getTextViewForTab(tabIndex);
    if (v != null) {
      // html
      v.paint(g, textRect);
    } else {
      // plain text
      int mnemIndex = tabPane.getDisplayedMnemonicIndexAt(tabIndex);

      g.setColor(ss.getStyle().getColor(ss, ColorType.TEXT_FOREGROUND));
      ss.getStyle().getGraphicsUtils(ss).paintText(ss, g, title,
          textRect, mnemIndex);
    }
  }


  private void paintContentBorder(SynthContext ss, Graphics g,
      int tabPlacement, int selectedIndex) {
    int width = tabPane.getWidth();
    int height = tabPane.getHeight();
    Insets insets = tabPane.getInsets();

    int x = insets.left;
    int y = insets.top;
    int w = width - insets.right - insets.left;
    int h = height - insets.top - insets.bottom;

    switch (tabPlacement) {
      case LEFT:
        x += calculateTabAreaWidth(tabPlacement, runCount, maxTabWidth);
        w -= (x - insets.left);
        break;
      case RIGHT:
        w -= calculateTabAreaWidth(tabPlacement, runCount, maxTabWidth);
        break;
      case BOTTOM:
        h -= calculateTabAreaHeight(tabPlacement, runCount, maxTabHeight);
        break;
      case TOP:
      default:
        y += calculateTabAreaHeight(tabPlacement, runCount, maxTabHeight);
        h -= (y - insets.top);
    }
    SynthLookAndFeel.updateSubregion(ss, g, new Rectangle(x, y, w, h));
    ss.getPainter().paintTabbedPaneContentBackground(ss, g, x, y,
        w, h);
    ss.getPainter().paintTabbedPaneContentBorder(ss, g, x, y, w, h);
  }

  private void ensureCurrentLayout() {
    if (!tabPane.isValid()) {
      tabPane.validate();
    }
        /* If tabPane doesn't have a peer yet, the validate() call will
         * silently fail.  We handle that by forcing a layout if tabPane
         * is still invalid.  See bug 4237677.
         */
    if (!tabPane.isValid()) {
      TabbedPaneLayout layout = (TabbedPaneLayout) tabPane.getLayout();
      layout.calculateLayoutInfo();
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected int calculateMaxTabHeight(int tabPlacement) {
    FontMetrics metrics = getFontMetrics(tabContext.getStyle().getFont(
        tabContext));
    int tabCount = tabPane.getTabCount();
    int result = 0;
    int fontHeight = metrics.getHeight();
    for (int i = 0; i < tabCount; i++) {
      result = Math.max(calculateTabHeight(tabPlacement, i, fontHeight), result);
    }
    return result;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected int calculateTabWidth(int tabPlacement, int tabIndex,
      FontMetrics metrics) {
    Icon icon = getIconForTab(tabIndex);
    Insets tabInsets = getTabInsets(tabPlacement, tabIndex);
    int width = tabInsets.left + tabInsets.right;
    Component tabComponent = tabPane.getTabComponentAt(tabIndex);
    if (tabComponent != null) {
      width += tabComponent.getPreferredSize().width;
    } else {
      if (icon != null) {
        width += icon.getIconWidth() + textIconGap;
      }
      View v = getTextViewForTab(tabIndex);
      if (v != null) {
        // html
        width += (int) v.getPreferredSpan(View.X_AXIS);
      } else {
        // plain text
        String title = tabPane.getTitleAt(tabIndex);
        width += tabContext.getStyle().getGraphicsUtils(tabContext).
            computeStringWidth(tabContext, metrics.getFont(),
                metrics, title);
      }
    }
    return width;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected int calculateMaxTabWidth(int tabPlacement) {
    FontMetrics metrics = getFontMetrics(tabContext.getStyle().getFont(
        tabContext));
    int tabCount = tabPane.getTabCount();
    int result = 0;
    for (int i = 0; i < tabCount; i++) {
      result = Math.max(calculateTabWidth(tabPlacement, i, metrics),
          result);
    }
    return result;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected Insets getTabInsets(int tabPlacement, int tabIndex) {
    updateTabContext(tabIndex, false, false, false,
        (getFocusIndex() == tabIndex));
    return tabInsets;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected FontMetrics getFontMetrics() {
    return getFontMetrics(tabContext.getStyle().getFont(tabContext));
  }

  private FontMetrics getFontMetrics(Font font) {
    return tabPane.getFontMetrics(font);
  }

  private void updateTabContext(int index, boolean selected,
      boolean isMouseDown, boolean isMouseOver, boolean hasFocus) {
    int state = 0;
    if (!tabPane.isEnabled() || !tabPane.isEnabledAt(index)) {
      state |= SynthConstants.DISABLED;
      if (selected) {
        state |= SynthConstants.SELECTED;
      }
    } else if (selected) {
      state |= (SynthConstants.ENABLED | SynthConstants.SELECTED);
      if (isMouseOver && UIManager.getBoolean("TabbedPane.isTabRollover")) {
        state |= SynthConstants.MOUSE_OVER;
      }
    } else if (isMouseOver) {
      state |= (SynthConstants.ENABLED | SynthConstants.MOUSE_OVER);
    } else {
      state = SynthLookAndFeel.getComponentState(tabPane);
      state &= ~SynthConstants.FOCUSED; // don't use tabbedpane focus state
    }
    if (hasFocus && tabPane.hasFocus()) {
      state |= SynthConstants.FOCUSED; // individual tab has focus
    }
    if (isMouseDown) {
      state |= SynthConstants.PRESSED;
    }

    tabContext.setComponentState(state);
  }

  /**
   * {@inheritDoc}
   *
   * Overridden to create a TabbedPaneLayout subclass which takes into
   * account tabOverlap.
   */
  @Override
  protected LayoutManager createLayoutManager() {
    if (tabPane.getTabLayoutPolicy() == JTabbedPane.SCROLL_TAB_LAYOUT) {
      return super.createLayoutManager();
    } else { /* WRAP_TAB_LAYOUT */
      return new TabbedPaneLayout() {
        @Override
        public void calculateLayoutInfo() {
          super.calculateLayoutInfo();
          //shift all the tabs, if necessary
          if (tabOverlap != 0) {
            int tabCount = tabPane.getTabCount();
            //left-to-right/right-to-left only affects layout
            //when placement is TOP or BOTTOM
            boolean ltr = tabPane.getComponentOrientation().isLeftToRight();
            for (int i = runCount - 1; i >= 0; i--) {
              int start = tabRuns[i];
              int next = tabRuns[(i == runCount - 1) ? 0 : i + 1];
              int end = (next != 0 ? next - 1 : tabCount - 1);
              for (int j = start + 1; j <= end; j++) {
                // xshift and yshift represent the amount &
                // direction to shift the tab in their
                // respective axis.
                int xshift = 0;
                int yshift = 0;
                // configure xshift and y shift based on tab
                // position and ltr/rtl
                switch (tabPane.getTabPlacement()) {
                  case JTabbedPane.TOP:
                  case JTabbedPane.BOTTOM:
                    xshift = ltr ? tabOverlap : -tabOverlap;
                    break;
                  case JTabbedPane.LEFT:
                  case JTabbedPane.RIGHT:
                    yshift = tabOverlap;
                    break;
                  default: //do nothing
                }
                rects[j].x += xshift;
                rects[j].y += yshift;
                rects[j].width += Math.abs(xshift);
                rects[j].height += Math.abs(yshift);
              }
            }
          }
        }
      };
    }
  }

  private class SynthScrollableTabButton extends SynthArrowButton implements
      UIResource {

    public SynthScrollableTabButton(int direction) {
      super(direction);
      setName("TabbedPane.button");
    }
  }
}
